Introduction to Networking in Flutter
Most mobile apps need to communicate with backend servers to fetch data, submit forms, and sync state. Flutter provides excellent support for HTTP networking through packages like http and dio. This chapter focuses on the standard http package for making REST API calls.
Key concept: All network operations in Flutter are asynchronous. Use async/await or Future chains to handle network responses without blocking the UI thread.
Setting Up the HTTP Package
Add Dependency
Add the http package to your pubspec.yaml:
pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.1.0
Import in Dart File
import 'package:http/http.dart' as http;
import 'dart:convert';
Making HTTP Requests
GET Request
Fetch data from a server:
Basic GET Request
Future fetchData() async {
final url = Uri.parse('https://api.example.com/users');
final response = await http.get(url);
if (response.statusCode == 200) {
// Success
final data = jsonDecode(response.body);
print(data);
} else {
// Error
print('Error: ${response.statusCode}');
}
}
POST Request
Send data to a server:
POST Request with JSON Body
Future createUser(String name, String email) async {
final url = Uri.parse('https://api.example.com/users');
final response = await http.post(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'name': name,
'email': email,
}),
);
if (response.statusCode == 201) {
final newUser = jsonDecode(response.body);
print('Created: $newUser');
}
}
PUT and DELETE Requests
Update and delete resources:
PUT and DELETE Examples
// Update user
Future updateUser(String userId, Map data) async {
final url = Uri.parse('https://api.example.com/users/$userId');
final response = await http.put(
url,
headers: {'Content-Type': 'application/json'},
body: jsonEncode(data),
);
}
// Delete user
Future deleteUser(String userId) async {
final url = Uri.parse('https://api.example.com/users/$userId');
final response = await http.delete(url);
if (response.statusCode == 204) {
print('User deleted');
}
}
Adding Headers and Authentication
Request Headers
Add custom headers for authentication, content type, and API keys:
Headers Example
Future fetchWithAuth(String token) async {
final url = Uri.parse('https://api.example.com/protected');
final response = await http.get(
url,
headers: {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'X-API-Key': 'your-api-key',
},
);
}
Query Parameters
Add query parameters to URLs:
Query Parameters
Future searchUsers(String query, int page) async {
final url = Uri.parse('https://api.example.com/users').replace(
queryParameters: {
'q': query,
'page': page.toString(),
'limit': '20',
},
);
final response = await http.get(url);
}
Parsing JSON Responses
Basic JSON Parsing
Convert JSON strings to Dart objects:
Simple JSON Parsing
// JSON string
final jsonString = '{"name": "John", "age": 30}';
// Parse to Map
final Map json = jsonDecode(jsonString);
print(json['name']); // John
print(json['age']); // 30
// Convert back to JSON
final String jsonOutput = jsonEncode(json);
Creating Model Classes
Create Dart classes to represent API data:
User Model Example
class User {
final String id;
final String name;
final String email;
User({
required this.id,
required this.name,
required this.email,
});
// Factory constructor from JSON
factory User.fromJson(Map json) {
return User(
id: json['id'] as String,
name: json['name'] as String,
email: json['email'] as String,
);
}
// Convert to JSON
Map toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
// Usage
Future> fetchUsers() async {
final response = await http.get(Uri.parse('https://api.example.com/users'));
if (response.statusCode == 200) {
final List jsonList = jsonDecode(response.body);
return jsonList.map((json) => User.fromJson(json)).toList();
} else {
throw Exception('Failed to load users');
}
}
Error Handling
Network Error Handling
Handle various types of network errors:
Comprehensive Error Handling
Future> fetchUsersSafe() async {
try {
final response = await http.get(
Uri.parse('https://api.example.com/users'),
).timeout(Duration(seconds: 10));
if (response.statusCode == 200) {
final List jsonList = jsonDecode(response.body);
return jsonList.map((json) => User.fromJson(json)).toList();
} else if (response.statusCode == 401) {
throw Exception('Unauthorized - please login');
} else if (response.statusCode == 404) {
throw Exception('Resource not found');
} else {
throw Exception('Server error: ${response.statusCode}');
}
} on SocketException {
throw Exception('No internet connection');
} on TimeoutException {
throw Exception('Request timeout - please try again');
} on FormatException {
throw Exception('Invalid response format');
} catch (e) {
throw Exception('Unexpected error: $e');
}
}
Required Imports
import 'dart:io'; // for SocketException
import 'dart:async'; // for TimeoutException
Building a Service Layer
API Service Pattern
Create a dedicated service class to centralize API calls:
ApiService Example
class ApiService {
static const String baseUrl = 'https://api.example.com';
String? _authToken;
void setAuthToken(String token) {
_authToken = token;
}
Map get _headers => {
'Content-Type': 'application/json',
if (_authToken != null) 'Authorization': 'Bearer $_authToken',
};
Future> getUsers() async {
final response = await http.get(
Uri.parse('$baseUrl/users'),
headers: _headers,
);
if (response.statusCode == 200) {
final List jsonList = jsonDecode(response.body);
return jsonList.map((json) => User.fromJson(json)).toList();
}
throw _handleError(response);
}
Future createUser(User user) async {
final response = await http.post(
Uri.parse('$baseUrl/users'),
headers: _headers,
body: jsonEncode(user.toJson()),
);
if (response.statusCode == 201) {
return User.fromJson(jsonDecode(response.body));
}
throw _handleError(response);
}
Exception _handleError(http.Response response) {
switch (response.statusCode) {
case 400:
return Exception('Bad request');
case 401:
return Exception('Unauthorized');
case 404:
return Exception('Not found');
case 500:
return Exception('Server error');
default:
return Exception('Error: ${response.statusCode}');
}
}
}
Loading States and UI Integration
Managing Loading States
Use StatefulWidget to manage loading, error, and data states:
Loading State Pattern
class UsersScreen extends StatefulWidget {
@override
_UsersScreenState createState() => _UsersScreenState();
}
class _UsersScreenState extends State {
List _users = [];
bool _isLoading = false;
String? _error;
final _apiService = ApiService();
@override
void initState() {
super.initState();
_loadUsers();
}
Future _loadUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final users = await _apiService.getUsers();
setState(() {
_users = users;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Error: $_error'),
ElevatedButton(
onPressed: _loadUsers,
child: Text('Retry'),
),
],
),
);
}
return ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.email),
);
},
);
}
}
Best Practices
Networking Best Practices
- Always handle errors and provide user-friendly error messages.
- Add timeouts to prevent indefinite waiting.
- Use try-catch blocks around network calls.
- Centralize API calls in a service layer for reusability.
- Validate JSON structure before parsing to avoid crashes.
- Store base URLs and endpoints as constants.
- Use proper HTTP status codes to determine success/failure.
- Implement retry logic for transient failures.
- Cache responses when appropriate to reduce network calls.
- Never make network calls in build() method.
Exercises
1. Basic API Client
Create an ApiService class that fetches a list of posts from JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts). Create a Post model class with id, title, and body fields. Display the posts in a ListView with proper loading and error states.
2. CRUD Operations
Extend the ApiService to support creating, updating, and deleting posts. Add UI buttons to test each operation and show success/error messages.
3. Error Handling and Retry
Implement a retry mechanism that attempts the request up to 3 times with exponential backoff. Add a retry button in the error UI that manually retries failed requests.